package main

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/joho/godotenv"
)

// PullRequest 结构体映射Gitee的pull_request字段
type PullRequest struct {
	Number int    `json:"number"`
	State  string `json:"state"`
	Title  string `json:"title"`
	Body   string `json:"body"`
	// Head和Base包含分支信息
	Head struct {
		Ref string `json:"ref"` // 源分支
	} `json:"head"`
	Base struct {
		Ref  string `json:"ref"` // 目标分支
		Repo struct {
			FullName          string `json:"full_name"`           // 如 "开源中国/Gitee"
			PathWithNamespace string `json:"path_with_namespace"` // 如 "oschina/gitee"
		} `json:"repo"`
	} `json:"base"`
}

// Enterprise 结构体映射Gitee企业版的enterprise字段
type Enterprise struct {
	ID   int    `json:"id"`   // 企业ID，如 4988464
	Name string `json:"name"` // 企业名称
	Path string `json:"path"` // 企业路径，如 "huajinet"
	URL  string `json:"url"`  // 企业URL
}

// Project 结构体映射Gitee企业版的project字段
type Project struct {
	ID                int    `json:"id"`                   // 项目ID，如 42548642
	Name              string `json:"name"`                 // 项目名称
	Path              string `json:"path"`                 // 项目路径
	PathWithNamespace string `json:"path_with_namespace"` // 完整路径
}

// Event 是Gitee webhook推送的完整结构
type Event struct {
	HookName    string      `json:"hook_name"` // 如 "merge_request_hooks"
	Action      string      `json:"action"`    // PR动作类型
	PullRequest PullRequest `json:"pull_request"`
	Enterprise  *Enterprise `json:"enterprise,omitempty"` // 企业版特有字段
	Project     *Project    `json:"project,omitempty"`    // 企业版项目信息
	Password    string      `json:"password"`              // webhook密码
}

func getenv(key, def string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return def
}

// verifySignature 验证 HMAC-SHA256 签名
// 与 Python 示例保持一致的签名算法
func verifySignature(secret string, timestamp int64, signature string) bool {
	// 构建待签名字符串：timestamp\nsecret
	stringToSign := fmt.Sprintf("%d\n%s", timestamp, secret)

	// 使用 HMAC-SHA256 生成签名
	h := hmac.New(sha256.New, []byte(secret))
	h.Write([]byte(stringToSign))
	digest := h.Sum(nil)

	// Base64 编码
	expectedSignature := base64.StdEncoding.EncodeToString(digest)

	// URL 编码（与 Python urllib.quote_plus 一致）
	// Python 的 quote_plus 会将空格转为 +，特殊处理
	expectedSignatureURLEncoded := url.QueryEscape(expectedSignature)
	expectedSignatureURLEncoded = strings.ReplaceAll(expectedSignatureURLEncoded, "+", "%2B")

	// 为了兼容性，同时支持多种编码格式
	expectedSignaturePlus := strings.ReplaceAll(expectedSignature, "+", "%2B")
	expectedSignaturePlus = strings.ReplaceAll(expectedSignaturePlus, "/", "%2F")
	expectedSignaturePlus = strings.ReplaceAll(expectedSignaturePlus, "=", "%3D")

	// 调试日志
	log.Printf("[DEBUG] Signature verification:\n  Timestamp: %d\n  String to sign: %s\n  Expected (base64): %s\n  Expected (url encoded): %s\n  Received: %s",
		timestamp, stringToSign, expectedSignature, expectedSignatureURLEncoded, signature)

	// 比较签名（支持多种编码格式）
	return signature == expectedSignature ||
		signature == expectedSignatureURLEncoded ||
		signature == expectedSignaturePlus
}

// isTimestampValid 检查时间戳是否在有效范围内（防止重放攻击）
func isTimestampValid(timestamp int64, maxAge int64) bool {
	now := time.Now().UnixNano() / 1e6 // 转为毫秒
	age := now - timestamp
	if age < 0 {
		age = -age // 允许轻微的时钟偏差
	}
	return age <= maxAge
}

// isProjectRoot 检查给定路径是否是项目根目录
func isProjectRoot(path string) bool {
	// 检查是否存在 start_review.sh 文件
	scriptPath := filepath.Join(path, "start_review.sh")
	if _, err := os.Stat(scriptPath); err == nil {
		return true
	}
	return false
}

func main() {
	// 加载 .env 文件（如果存在）
	loadEnvFile()

	// 获取可执行文件所在目录，用于计算相对路径
	execPath, err := os.Executable()
	if err != nil {
		log.Printf("[WARN] Failed to get executable path: %v", err)
		execPath = "."
	}
	execDir := filepath.Dir(execPath)
	
	// 计算项目根目录（webhook-runner 的上级目录）
	projectRoot := filepath.Join(execDir, "..")
	if !isProjectRoot(projectRoot) {
		// 如果不是从 webhook-runner 目录运行，假设当前目录就是项目根目录
		projectRoot = "."
	}
	
	// 基本配置（可用环境变量覆盖）
	secret := getenv("WEBHOOK_SECRET", "")                                    // 可在 Gitee Webhook 里用"自定义密码/密钥"，这里用来比对
	defaultScript := filepath.Join(projectRoot, "start_review.sh")
	script := getenv("SCRIPT_PATH", defaultScript)                           // 你的评审脚本
	defaultRepo := getenv("DEFAULT_REPO", "chazzorg/batch_ssh")               // 兜底仓库
	
	// 端口配置：支持 LISTEN_ADDR 和 LISTEN_PORT 环境变量
	listenAddr := getenv("LISTEN_ADDR", "0.0.0.0")                           // 监听地址，默认 0.0.0.0
	listenPort := getenv("LISTEN_PORT", "9002")                             // 监听端口，默认 9002
	addr := listenAddr + ":" + listenPort                                    // 组合成完整地址
	
	jobTimeout := time.Duration(getInt("JOB_TIMEOUT_SEC", 900)) * time.Second // 单次脚本最长运行时间，默认15分钟
	useSignature := getenv("USE_SIGNATURE", "false") == "true"                // 是否使用签名验证
	maxTimestampAge := int64(getInt("MAX_TIMESTAMP_AGE_MS", 300000))          // 时间戳最大有效期，默认5分钟

	mux := http.NewServeMux()
	
	// 健康检查端点
	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		fmt.Fprint(w, "healthy")
	})
	
	mux.HandleFunc("/gitee-webhook", func(w http.ResponseWriter, r *http.Request) {
		// 先读取body以供后续使用
		body, err := io.ReadAll(r.Body)
		if err != nil {
			http.Error(w, "bad request", http.StatusBadRequest)
			return
		}
		_ = r.Body.Close()

		// 1) 鉴权：支持签名验证或简单密码验证
		if sec := secret; sec != "" {
			if useSignature {
				// 签名验证模式（更安全）
				// 优先从 Gitee 常见头获取；兼容多种可能的签名头
				timestampStr := r.Header.Get("X-Gitee-Timestamp")
				signature := r.Header.Get("X-Gitee-Token") // 一些场景下签名或密钥放在此头
				if signature == "" {
					signature = r.Header.Get("X-Gitee-Sign") // 兼容 X-Gitee-Sign
				}
				if signature == "" {
					signature = r.Header.Get("X-Gitee-Signature") // 兼容 X-Gitee-Signature
				}

				// 如果 Header 中没有，尝试从 URL 参数获取
				if timestampStr == "" {
					timestampStr = r.URL.Query().Get("timestamp")
				}
				if signature == "" {
					signature = r.URL.Query().Get("sign")
				}

				// 解析时间戳
				timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
				if err != nil {
					log.Printf("[WARN] Invalid timestamp: %s", timestampStr)
					http.Error(w, "invalid timestamp", http.StatusUnauthorized)
					return
				}

				// 验证时间戳有效性（防重放攻击）
				if !isTimestampValid(timestamp, maxTimestampAge) {
					log.Printf("[WARN] Timestamp expired: %d", timestamp)
					http.Error(w, "timestamp expired", http.StatusUnauthorized)
					return
				}

				// 验证签名
				if !verifySignature(sec, timestamp, signature) {
					log.Printf("[WARN] Signature verification failed")
					http.Error(w, "invalid signature", http.StatusUnauthorized)
					return
				}

				log.Printf("[INFO] Signature verification passed")
			} else {
				// 简单密码验证模式（向后兼容）
				// 优先兼容 Gitee 的 X-Gitee-Token，其次兼容 X-Webhook-Token
				token := r.Header.Get("X-Gitee-Token")
				if token == "" {
					token = r.Header.Get("X-Webhook-Token")
				}
				if token == "" {
					// 方式2: URL参数中的token
					token = r.URL.Query().Get("token")
				}
				if token == "" {
					// 方式3: 尝试从body中读取password字段
					var tempEvt struct {
						Password string `json:"password"`
					}
					if err := json.Unmarshal(body, &tempEvt); err == nil {
						token = tempEvt.Password
					}
				}
				if token != sec {
					log.Printf("[WARN] Authentication failed: token mismatch")
					http.Error(w, "unauthorized", http.StatusUnauthorized)
					return
				}
			}
		}

		// 保存webhook数据到logs文件夹（用于调试）
		go saveWebhookData(body, projectRoot)

		// 3) 尝试解析为"PR 事件"，获取 repo 与 PR 编号。解析失败也不会中断（见兜底）
		var evt Event
		_ = json.Unmarshal(body, &evt)

		// 从base.repo中获取仓库路径，支持两种格式
		repo := evt.PullRequest.Base.Repo.PathWithNamespace
		if repo == "" {
			// 备选：使用full_name
			repo = evt.PullRequest.Base.Repo.FullName
		}
		if repo == "" {
			repo = defaultRepo
		}
		prNumber := evt.PullRequest.Number
		if prNumber == 0 {
			// 兜底：允许通过 querystring 覆盖 ?pr=1
			if qs := r.URL.Query().Get("pr"); qs != "" {
				if n, err := strconv.Atoi(qs); err == nil {
					prNumber = n
				}
			}
		}

		if prNumber == 0 {
			// 没拿到 PR 编号就直接返回 202（忽略），避免误触
			log.Printf("[INFO] No PR number found, ignoring webhook")
			w.WriteHeader(http.StatusAccepted)
			fmt.Fprint(w, "ignored: no PR number")
			return
		}

		// 记录收到的webhook信息
		log.Printf("[INFO] Received webhook: hook_name=%s, repo=%s, pr=%d, action=%s",
			evt.HookName, repo, prNumber, evt.Action)

		// 过滤PR事件：只处理新建和重新打开的PR，跳过关闭、合并等操作
		if evt.Action != "open" && evt.Action != "reopen" {
			log.Printf("[INFO] Ignoring PR action '%s', only processing 'open' and 'reopen' events", evt.Action)
			w.WriteHeader(http.StatusAccepted)
			fmt.Fprintf(w, "ignored: action=%s (only 'open' and 'reopen' events are processed)", evt.Action)
			return
		}

		// 检测是否为企业版（有enterprise字段且ID大于0）
		isEnterprise := evt.Enterprise != nil && evt.Enterprise.ID > 0
		var enterpriseID, projectID int
		var enterprisePath string
		
		if isEnterprise {
			enterpriseID = evt.Enterprise.ID
			enterprisePath = evt.Enterprise.Path
			if evt.Project != nil {
				projectID = evt.Project.ID
			}
			log.Printf("[INFO] Detected enterprise version: enterprise_id=%d, enterprise_path=%s, project_id=%d", 
				enterpriseID, enterprisePath, projectID)
		}

		// 4) 异步触发脚本（非阻塞回复），把关键信息通过参数+环境变量传递
		go func(repo string, pr int, entID int, projID int, entPath string, raw string) {
			ctx, cancel := context.WithTimeout(context.Background(), jobTimeout)
			defer cancel()

			// 构建命令参数
			args := []string{script, "--repo", repo, "--pr", strconv.Itoa(pr)}
			
			// 如果是企业版，添加额外参数
			if entID > 0 {
				args = append(args, 
					"--enterprise-id", strconv.Itoa(entID),
					"--enterprise-path", entPath)
				if projID > 0 {
					args = append(args, "--project-id", strconv.Itoa(projID))
				}
			}

			cmd := exec.CommandContext(ctx, "/bin/bash", args...)
			// 设置工作目录为项目根目录
			cmd.Dir = projectRoot
			// 传一些有用的环境变量给脚本（可选）
			envVars := []string{
				"WEBHOOK_EVENT=pr",
				"WEBHOOK_REPO=" + repo,
				"WEBHOOK_PR=" + strconv.Itoa(pr),
			}
			if entID > 0 {
				envVars = append(envVars,
					"WEBHOOK_ENTERPRISE_ID=" + strconv.Itoa(entID),
					"WEBHOOK_ENTERPRISE_PATH=" + entPath,
					"WEBHOOK_PROJECT_ID=" + strconv.Itoa(projID),
				)
			}
			cmd.Env = append(os.Environ(), envVars...)
			
			out, err := cmd.CombinedOutput()
			if err != nil {
				log.Printf("[ERROR] script failed repo=%s pr=%d err=%v out=\n%s", repo, pr, err, string(out))
			} else {
				log.Printf("[OK] script finished repo=%s pr=%d out=\n%s", repo, pr, string(out))
			}
		}(repo, prNumber, enterpriseID, projectID, enterprisePath, string(body))

		// 立即响应，避免重试
		w.WriteHeader(http.StatusOK)
		fmt.Fprintf(w, "ok: %s#%d\n", repo, prNumber)
	})

	server := &http.Server{
		Addr:              addr,
		Handler:           mux,
		ReadHeaderTimeout: 10 * time.Second,
	}

	// 打印启动信息
	log.Printf("=== PR Sentinel Webhook Server ===")
	log.Printf("Listening on %s (path: /gitee-webhook)", addr)
	log.Printf("Working directory: %s", projectRoot)
	log.Printf("Script path: %s", script)
	if secret != "" {
		if useSignature {
			log.Printf("Authentication: HMAC-SHA256 signature (timestamp window: %dms)", maxTimestampAge)
		} else {
			log.Printf("Authentication: Simple password")
		}
	} else {
		log.Printf("WARNING: No authentication configured!")
	}
	log.Printf("===================================")

	log.Fatal(server.ListenAndServe())
}

func getInt(key string, def int) int {
	if v := os.Getenv(key); v != "" {
		if n, err := strconv.Atoi(v); err == nil {
			return n
		}
	}
	return def
}

func loadEnvFile() {
	// 获取可执行文件所在目录
	execPath, err := os.Executable()
	var execDir string
	if err == nil {
		execDir = filepath.Dir(execPath)
	} else {
		execDir = "."
	}
	
	// 尝试多个可能的 .env 文件位置
	possiblePaths := []string{
		".env",                                           // 当前目录
		filepath.Join(execDir, ".env"),                  // 可执行文件所在目录
		filepath.Join(execDir, "..", ".env"),            // 项目根目录（如果从webhook-runner运行）
		"../.env",                                       // 上级目录
		filepath.Join(os.Getenv("HOME"), ".pr-sentinel.env"), // 用户目录
	}

	for _, path := range possiblePaths {
		if err := godotenv.Load(path); err == nil {
			absPath, _ := filepath.Abs(path)
			log.Printf("Loaded environment from: %s", absPath)
			return
		}
	}

	// 如果没有找到 .env 文件，不报错，继续使用系统环境变量
	log.Println("No .env file found, using system environment variables")
}

// saveWebhookData 保存webhook推送数据到logs文件夹
func saveWebhookData(data []byte, projectRoot string) {
	// 确保logs目录存在
	logsDir := filepath.Join(projectRoot, "logs", "webhooks")
	if err := os.MkdirAll(logsDir, 0755); err != nil {
		log.Printf("[ERROR] Failed to create logs directory: %v", err)
		return
	}

	// 生成带时间戳的文件名
	timestamp := time.Now().Format("20060102-150405")
	filename := filepath.Join(logsDir, fmt.Sprintf("webhook-%s.json", timestamp))

	// 美化JSON格式（如果可能）
	var prettyJSON []byte
	var jsonObj interface{}
	if err := json.Unmarshal(data, &jsonObj); err == nil {
		if pretty, err := json.MarshalIndent(jsonObj, "", "  "); err == nil {
			prettyJSON = pretty
		}
	}
	if len(prettyJSON) == 0 {
		prettyJSON = data
	}

	// 保存数据
	if err := ioutil.WriteFile(filename, prettyJSON, 0644); err != nil {
		log.Printf("[ERROR] Failed to save webhook data: %v", err)
	} else {
		log.Printf("[INFO] Webhook data saved to: %s", filename)
	}
}
